Typescript 5.3 / 5.4
Nachdem mit Angular 17 die TypeScript-Versionen ab 5.2 verwendet werden können, muss mit Angular 18 nun mindestens TypeScript in Version 5.4 genutzt werden. Die Versionen 5.3 und 5.4 enthalten jeweils viele Verbesserungen in Bezug auf Performance und Type-Inference bzw. Type-Erkennung. Außerdem wurden mit den beiden Versionen viele neue Features eingeführt. Beispielhaft sollen einige der Neuerungen an dieser Stelle erklärt werden.
Type-Narrowing bei Switch-Statements
Um genauere Entscheidungen über verwendete Typen treffen zu können, beherrscht TypeScript das Type-Narrowing. Type-Narrowing kann genutzt werden, um aus einem allgemeineren Typen einen spezielleren abzuleiten. Umgesetzt wird Type-Narrowing in TypeScript typischerweise durch sogenannte Type-Guards. In Listing 1 bekommt beispielsweise die Funktion getColors() den Parameter status übergeben, dessen Typ entweder ein Array verschiedener Status-Elemente sein kann, ein einzelner Status oder null. Soll nun auf ein Property des Status zugegriffen werden, etwa status.color, so ist das nicht direkt möglich, da nicht definiert ist, ob ein Status-Objekt vorliegt, mehrere Objekte in einem Array oder gar null. Zur Fallunterscheidung werden in Listing 1 if-Bedingungen verwendet, die als Type-Guards dienen. Innerhalb der if-Blöcke ist die Variable jetzt auf den entsprechenden Typ “eingeengt”, daher der Begriff “Type-Narrowing”.
Listing 1: Type-Narrowing mit If-Bedingungen
function getColors(status: Status[] | Status | null): string[] {
if(!status) {
return []; // `status` ist `null`
}
if(Array.isArray(status)) {
return status.map(stat => stat.color) // `status` ist ein `Array<Status>`
}
return [status.color] // `status` ist ein `Status`-Objekt
}
Mit TypeScript 5.3 wurde nun die Möglichkeit geschaffen, Type-Narrowing in Verbindung mit switch(true)-Ausdrücken zu verwenden. Bei switch(true)-Ausdrücken wird jeweils immer nur der Pfad ausgeführt, der momentan true ergibt. Daher sollte in einem solchen Fall auch Type-Narrowing möglich sein, dies wurde mit TypeScript 4.3 umgesetzt. Ein Beispiel dazu ist in Listing 2 gezeigt. Jeder Branch der Switch-Anweisung ist durch einen eigenen Type-Guard abgesichert, sodass ein sicherer Zugriff auf die jeweiligen Eigenschaften möglich ist. Ein solches Statement kann nun also grundsätzlich wie eine if-elseif-else-Verkettung verwendet werden, je nach Entwicklerpräferenz.
Listing 2: Type-Narrowing mit switch(true)
function getColors(status: Status[] | Status | null): string[] {
switch (true) {
case !status: {
return []; // `status` ist `null`
}
case Array.isArray(status): {
return status.map(stat => stat.color) // `status` ist ein `Array<Status>`
}
default: {
return [status.color] // `status` ist ein `Status`-Objekt
}
}
}
Object.groupBy und Map.groupBy
Mit dem neuesten ECMAScript-Standard ES2024 wird zu den bestehenden Klassen Object und Map jeweils die neue Methode groupBy() hinzugefügt. Diese kann verwendet werden, um die Einträge eines Iterable, also z.B. eines Array, anhand eines Kriteriums zu gruppieren und in ein Objekt zu überführen. In Listing 3 ist gezeigt, wie groupBy genutzt werden kann, um ein Array mit Zahlen in ein Array für gerade und eines für ungerade Zahlen aufzuspalten. Dazu bekommt die groupBy-Funktion das Array numberArray mit Zahlen übergeben und eine Iterator-Funktion, die jedem Wert im Array eine Gruppe zuweist. In diesem Fall wird mit Hilfe des Modulo-Operator geprüft, ob der Wert gerade oder ungerade ist und entsprechend die Gruppe even oder odd vergeben. Das Resultat ist ein Objekt, welches die Eigenschaften even und odd hat, wobei sich in einem Property das gerade, im anderen Property das ungerade Array befindet. Beide Properties müssen mit dem Safe-Navigation-Operator (auch Elvis-Operator, “?.”) aufgerufen werden, da ein Property nur dann angelegt wird, wenn die entsprechende Gruppe auch Einträge hat.
Von der Syntax her genau wie Object.groupBy() funktioniert auch Map.groupBy(), jedoch wird im Falle von Map.groupBy() eine Map zurückgegeben, deren Keys in diesem Fall even und odd wären. Anders als bei Objekten können bei Maps nicht nur Strings, sondern im Prinzip beliebige Objekte als Key dienen.
Listing 3: Verwendung von Object.groupBy()
const numberArray = [0, 1, 2, 3, 4, 5];
const evenOddObj = Object.groupBy(numberArray, (num) => {
return num % 2 === 0 ? "even": "odd";
});
// Resultat ist Objekt mit Properties "even" und "odd"
console.log(evenOddObj?.even); // ->[0, 2, 4]
console.log(evenOddObj?.odd); // ->[1, 3, 5]
Einschränkungen bei Enums
Bisher war es möglich, in TypeScript-Enums die vorbelegten TypeScript-Keywords “Infinity”, “-Infinity” und “NaN” als Enum-Key zu verwenden, siehe Listing 4. Dies ist mit TypeScript 5.4 nicht mehr möglich.
Listing 4: Mit TypeScript 5.4 nicht mehr erlaubte Enum-Keys
//Errors mit diesen Keys
enum NotAllowsEnumKeys {
Infinity,
"-Infinity",
NaN
}
Angular CLI
Bereits mit den Angular-CLI Versionen 17.1/17.2/17.3 sind einige erwähnenswerte Neuerungen in Angular eingeflossen. Diese sollen hier auf dem Weg von Angular 17 zu Angular 18 ebenfalls kurze Erwähnung finden.
Angular CLI 17.1, 17.2 und 17.3
Der Karma-Testrunner, den Angular-CLI für das Ausführen der Unit-Tests nutzt, wird mittlerweile nicht mehr weiterentwickelt. Daher ist das Angular-Team dabei, als Ersatz für Karma entweder das Jest-Testframework anzubieten, oder den @web/test-runner. Mit Jest laufen die Tests dabei rein in der Node.js-Umgebung, während der Web-Test-Runner die Tests – wie von Karma gewohnt – in der Browser-Laufzeitumgebung ausführt. Der Jest-Support kam als Preview bereits mit Angular 17, mit 17.1 kam dann der erste (experimenteller) Support für den Web-Test-Runner, nämlich in Form eines eigenen Test-Builders (@angular-devkit/build-angular:web-test-runner). Das Angular-Team ist also weiter fleißig dabei, das Unit-Testing mit Angular in eine neue Zeit zu bringen.
Eine weitere Anpassung an moderne Web-Entwicklungsumgebungen ist die neue Möglichkeit, Angular-Projekte nicht nur mit NPM- oder Yarn-Paketmanagern erzeugen und nutzen zu können, sondern auch mit Bun. Bun ist ein neuer Paketmanager im NPM-Umfeld, der deutlich schneller als NPM und Yarn sein soll.
Für eine bessere Übersicht beim Entwickeln ist außerdem ermöglicht worden, den Terminalinhalt zwischen Rebuilds zu leeren. Dazu muss lediglich die neue Option clearScreen in der angular.json angegeben werden, wie in Listing 5 dargestellt.
Listing 5: “clearScreen”-Option in der angular.json
{
"projects": {
"clear-demo": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"clearScreen": true
}
}
}
}
}
}
Angular CLI 18
Die wichtigste Neuerung im Angular-CLI 18 ist, dass Version 22 der Node.js-Laufzeitumgebung unterstützt wird. Damit können Entwickler auch auf die neueste Version der Node.js-Laufzeit im Rahmen der Angular-Entwicklung zurückgreifen. Daneben gibt es einige Erweiterungen, die das Entwickler-Leben einfacher machen sollen. Zum Beispiel hat das Kommando ng serve einen neuen Alias bekommen. Bisher gab es beispielsweise den Alias “ng s”, mit dem der Dev-Server gestartet werden konnte, neu dazu gekommen ist nun “ng dev”.
Gerade bei neuen Entwicklern konnte es laut Angular-Team unter Umständen zu Verwirrungen um den assets-Ordner in Angular-Projekten kommen. Dieser Ordner ist explizit für statische (Laufzeit-)Assets gedacht, während die Build-Zeit-Assets unterhalb des src-Ordner abgelegt sein sollten. Um diesen Unterschied klarer zu machen, heißt der Ordner bei neu generierten Projekten in Zukunft deshalb nicht mehr assets, sondern public.
Weiterhin wird mit Angular-CLI 18 der Befehl ng doc entfernt, da dieser ohnehin nur selten genutzt wurde. Die offizielle Empfehlung ist, stattdessen auf der Seite “https://angular.dev/” nachzuschauen.
Breaking Changes
Neben der neuen unterstützten Node.js-Version 22 wurde auch die minimal notwendige Node.js-Version angepasst. Die minimal notwendige LTS-Node.js-Version ist nun 18.19.1. Node.js 19 wird gar nicht mehr unterstützt und Node.js 20 erst ab Version 20.11.1.
Weiterhin wurde die Anbindung der alten, nur noch im Webpack-Build unterstützten Sass-API (“NG_BUILD_LEGACY_SASS”) entfernt. Das Legacy-API ermöglichte beispielsweise noch die alte “~”-Import-Syntax.
Angular 18
Auch mit den minor Angular-17-Versionen sind einige interessante Neuerungen in Bezug auf die Zukunft von Angular hinzugekommen. Das betrifft zum einen die Weiterentwicklung des Frameworks im Allgemeinen. Zum anderen sind vor allem die Angular-Signal-APIs deutlich erweitert worden, um so das Ziel zu erreichen, Angular-Anwendungen in Zukunft ohne zone.js laufen lassen zu können. Die Neuerungen sollen an dieser Stelle kurz vorgestellt werden.
Angular 17.1, 17.2 und 17.3
Ganz besondere Aufmerksamkeit haben die Signal-APIs innerhalb der letzten Angular-Versionen erhalten. Dazu gehören vor allem APIs, mit denen die Decorators innerhalb von Komponenten durch Signal-Varianten ersetzt werden können. Dies gilt vor allem für Komponenten bzw. Anwendungen, die künftig ohne Zone.js auskommen sollen. Um die Neuerungen zu veranschaulichen, ist in Listing 6 die Komponente DemoFormComponent gezeigt, die ein boolesches @Input für den Nachtmodus und ein String-@Input für den momentanen Formularwert hat. Das Input für den Formularwert hat außerdem das Flag required, es wird also ein Compiler-Fehler ausgegeben, wenn dieses Input nicht gebunden ist. Außerdem ist ein @Output mit dem Namen valueChange vorhanden, das bei jeder Wertänderung des Formulars emittiert. Zu jedem @Output gehört immer ein EventEmitter. (Da der EventEmitter jedoch vom RxJS-Subject erbt, könnte an dieser Stelle auch ein Observable stehen, dazu später mehr.)
Da der Input value und der Output valueChange heißt, können beide per Two-Way-Databinding angebunden werden:
<app-demo-form [(value)]=“firstname“></app-demo-form>
Die Variable firstname kann entweder eine normale String-Variable sein, oder wiederum ein signal(“).
Listing 6: Herkömmliche Angular-Standalone-Komponente mit Inputs und Outputs
@Component({
selector: 'app-demo-form',
standalone: true,
imports: [FormsModule],
template: `
<div [class.night-mode]="nightMode">
<input type="text"
[ngModel]="value"
(ngModelChange)="valueChanged.emit($event)">
</div>`
})
export class DemoFormComponent {
@Input() nightMode?: boolean;
@Input({required: true}) value!: string;
@Output() valueChange = new EventEmitter<string>();
}
In Listing 7 ist im Vergleich dazu eine Komponente, die auf den neuen Signal-APIs basiert. Der “nightMode”-Input hat nun keinen “@Input”-Decorator mehr, stattdessen wird die “input()”-Funktion verwendet. Dabei ist zu beachten, dass beim Input zwar der Typparameter “boolean” angegeben ist, jedoch das Signal eigentlich vom Typ “boolean | undefined”. Der “undefined”-Fall tritt dann ein, wenn von außen kein Wert an das Input gebunden wird. Ansonsten erzeugt die “input()”-Funktion ein Signal, das immer den aktuell von außen gebundenen Input-Wert enthält. Immer wenn von außen ein neuer Wert gebunden wird, wird auch das Template aktualisiert. Ein Input-Signal kann wie alle Signals auch als Quelle für ein “computed()”-Signal genutzt werden. “computed()”-Signals entsprechen somit Properties, die man ohne die Signal-APIs innerhalb des “ngOnChanges()”-Hook schreiben würde.
Das Two-Way-Databinding vereinfacht sich mit der Signal-API deutlich, denn es wird kein separates Input/Output mehr benötigt, stattdessen wird ein einfaches ModelSignal erstellt, das sowohl Input als auch Output in sich vereint.
Im Beispiel von Listing 7 ist auch die neue “.required”-API gezeigt, die dem Required-Flag bei den @Input-Decorators entspricht. Im Gegensatz zu den Decorators wirkt sich das Required bei den Signals auch auf den Typen aus: Ein “input<boolean>()” enthält Werte mit Typ “boolean | undefined”, während ein “input.required<boolean>()” immer ein echtes “boolean” enthält. Das gleiche gilt für “model” und “model.required”.
Listing 7: Angular-Standalone-Komponente mit Signal-Input und -Model
@Component({
selector: 'app-demo-form,
standalone: true,
imports: [FormsModule],
template: `
<div [class.night-mode]="nightMode()">
<input type="text" [(ngModel)]="value">
</div>`
})
export class DemoFormComponent {
nightMode = input<boolean>();
value = model.required<string>();
}
Neben dem “@Input”-Decorator haben auch die “@ViewChild”-, “@ViewChildren”-, “@ContentChild”- und “@ContentChildren”-Decorators eine neue Signal-API bekommen. In Listing 8 ist eine Komponente gezeigt, wie sie z.B. zur Darstellung von Graphen bzw. Charts genutzt werden könnte. Um das HTMLCanvasElement aus dem Template heraus an die Charting-Library zu übergeben, wird der “@ViewChild”-Decorator verwendet. Außerdem wird, sobald der Chart gerendert ist, der “chartLoaded”-EventEmitter getriggert, um ein Angular-”@Output”-Event auszulösen.
Listing 8: Angular-Komponente mit Canvas, Output und ViewChild
@Component({
selector: 'app-demo-chart',
standalone: true,
template: `<canvas #canvasEl></canvas>`
})
export class DemoChartComponent implements AfterViewInit {
@ViewChild('canvasEl') canvas!: ElementRef<HTMLCanvasElement>;
@Output() chartLoaded = new EventEmitter<string>();
ngAfterViewInit(): void {
const context = this.canvas.nativeElement.getContext('2d');
const chart = chartingLib.drawChart(context);
chart.onChartReady = chart => {
this.chartLoaded.emit(chart.id);
};
}
}
In Listing 9 ist die gleiche Komponente gezeigt, diesmal jedoch mit der entsprechenden Signal-API. Die Variable “canvas” bekommt nun keinen Decorator, stattdessen wird die “viewChild()”-Funktion aufgerufen, die auch “Query-Funktion” genannt wird, da mit diesen Funktionen Elemente des Template abgefragt (engl. ge-queried) werden können. Analog zum ViewChild gibt es die Query-Funktionen “viewChildren()”-, “contentChild()”- und “contentChildren()”. Im Beispiel von Listing 9 ist außerdem gezeigt, dass die Query-Funktion “viewChild” analog zum Input die “.required”-API mitbringen. Das bedeutet, dass der Typ des ViewChild in diesem Fall nicht “Signal<ElementRef<HTMLCanvasElement> | undefined>” ist, sondern nur “Signal<ElementRef<HTMLCanvasElement>>” – also ohne undefined. Das funktioniert aber nur, wenn die Template-Referenz-Variable (hier “canvasEl”) immer direkt verfügbar ist. Ansonsten gibt es einen Laufzeitfehler.
Die Children-Query-Funktionen haben keine Required-API, stattdessen liegen die einzelnen ElementRefs hier als Array vor. Wenn es kein Child gibt, auf das die Query passt, enthält das Signal einfach ein leeres Array. Der Datentyp von “viewChildren<ElementRef<HTMLCanvasElement>>(‚canvasEl‘)” wäre somit “Signal<ElementRef<HTMLCanvasElement>[]>”.
Ein weiteres Detail in Listing 9 ist die neue Output-API: Statt “@Output”-Decorator und “EventEmitter” kann einfach die Funktion “output()” verwendet werden. Die neue API baut auch nicht mehr auf dem EventEmitter auf, stattdessen liefert “output()” eine “OutputEmitterRef” zurück. Das wurde auch deshalb gemacht, um die API etwas unabhängiger von RxJS zu machen, denn der alte EventEmitter ist im Kern ein RxJS-Subject. Diese Eigenschaft wurde von einigen Entwicklern so interpretiert, dass statt des EventEmitters ein “normales” Observable an die “@Output”-Felder gebunden wurde. Um die Nutzung der Outputs mit Observables auch in Zukunft zu ermöglichen, kann in der neuen API statt “output()” die Funktion “outputFromObservable(this.demoObservable)” verwendet werden. Dabei ist “this.demoObservable“ das Observable. Der Output emittiert dann jedesmal ein Event, wenn auch das DemoObservable einen Wert emittiert.
Die neue API ist zwar nicht direkt eine Signal-API, wurde aus Konsistenzgründen allerdings so konzipiert, dass sie wie die neuen Signals-APIs aussieht und auch so verwendbar ist.
Eine wichtige Anmerkung zu den neuen Signal-Komponenten-APIs ist, dass sie bisher noch in Developer-Preview sind. Das bedeutet, dass sie im Allgemeinen schon Produktionsqualität haben, allerdings kann sich im Detail die eine oder andere API noch leicht verändern. Deshalb sollten diese APIs ggf. noch nicht in produktiven Anwendungen eingesetzt werden.
Listing 9: Angular-Komponente mit Canvas und Signal-APIs
@Component({
selector: 'app-demo-chart',
standalone: true,
template: `<canvas #canvasEl></canvas>`
})
export class DemoChartComponent {
canvas = viewChild.required<ElementRef<HTMLCanvasElement>>(
'canvasEl'
);
chartLoaded = output<string>();
constructor() {
afterNextRender(() => {
const context = this.canvas().nativeElement.getContext('2d');
const chart = chartingLib.drawChart(context);
chart.onChartReady = chart => {
this.chartLoaded.emit(chart.id);
};
}, {phase: AfterRenderPhase.Write});
}
}
Angular 18
Um die begonnene Angular-Renaissance zu festigen, sind mit Version 18 zunächst die neuen Control-Flow-APIs und die Deferrable-Views als “stable” markiert worden. Angular 18 bringt außerdem viele unterschiedliche neue Features mit. Einige sind im Rahmen der “Angular Renaissance” und den neuen Signal-APIs zu betrachten. Dazu zählt zum Beispiel die Möglichkeit, die App komplett ohne Zone.js laufen zu lassen, sodass nur noch die Signals für die Change-Detection sorgen. Dazu kann der neue ZonelessChangeDetection-Provider beim App-Bootstrap verwendet werden, siehe Listing 10. Es sollte aber beachtet werden, dass dieser noch als experimentell markiert ist und daher nicht in produktiven Anwendungen verwendet werden sollte.
Listing 10: Bootstrap der Anwendung mit ZonelessChangeDetection
bootstrapApplication(AppComponent, {providers: [
provideExperimentalZonelessChangeDetection(),
]});
In Angular kann der sogenannte Content-Projection-Mechanismus für eine bessere Komposition verschiedener Ober- und Unterkomponenten verwendet werden. Das wird häufig verwendet, um Komponenten zu bauen, die zur Strukturierung von Inhalten gedacht sind. Etwa eine Modal-Komponente, die das Modal in Header, Content und Footer aufteilt; oder eine ListComponent, die per Content-Projection die einzelnen ListItemComponents entgegen nimmt. In Listing 11 ist eine “DemoPageComponent” gezeigt, die die Struktur einer Seite vorgibt. Hier wird die Seite einfach in einen separaten Header- und Content-Bereich aufgeteilt (das Template ist hier bewusst einfach gehalten, um das Beispiel nicht zu verkomplizieren). Das Neue mit Angular 18 ist nun, dass die “”-Slots einen eigenen (Default-)Inhalt haben. Vor Angular 18 war dies nicht möglich. Nun wird zur Laufzeit der Default-Inhalt angezeigt, wenn kein Inhalt übergeben wird. Das Laufzeit-Resultat des Beispiel aus Listing 11 sähe damit in etwa aus wie in Listing 12 gezeigt.
Listing 11: Default-Content für Content-Projection
@Component({
selector: 'demo-page',
template: `
<ng-content select="header">Default Header</ng-content>
<br>
<ng-content select="main">Default Content</ng-content>
`
})
export class DemoPageComponent {}
@Component({
template: `
<demo-page>
<main>My Demo Content</main>
</demo-page>
`
})
export class AppComponent {}
Listing 12: DOM-Struktur zur Laufzeit
<app-root>
<demo-page>
Default Header
<br>
<main>My Demo Content</main>
</demo-page>
</app-root>
In der Angular Forms-API ist ein lange nachgefragtes Feature hinzugekommen: Auf den FormControls bzw. FormGroups gibt jetzt nicht mehr nur die ValueChange- und StatusChange-Observables (“formControl.valueChanges” bzw. “formControl.statusChanges”). Neuerdings gibt es auch ein allgemeines Events-Observable (“formControl.events”). Dies kann spezielle neue Events emittieren:
- PristineChangeEvent (Control wechselt zwischen Pristine und Dirty)
- TouchedChangeEvent (Control wechselt zwischen Untouched und Touched)
- ValueChangeEvent (Wert-Änderung auf einem Control)
- StatusChangeEvent (Änderung des Control-Status)
- Status kann sein :
‚VALID‘ | ‚INVALID‘ | ‚PENDING‘ | ‚DISABLED‘
Ein weiteres, häufig gewünschtes Feature wird ebenfalls durch die neuen ChangeEvents ermöglicht: Jedes Event enthält das Property “source”, das diejenige FormControl oder FormGroup enthält, die das Event ausgelöst hat. Das kann sehr hilfreich sein, gerade in größeren Formularen: Wenn etwa zentral überprüft werden soll, welche einzelne FormControl sich geändert hat und dadurch eine ganze FormGroup von “valid” auf “invalid” gewechselt ist. Das “events”-Observable kann durch InstanceOf-Checks gefiltert werden, etwa folgendermaßen:
control.events.pipe(filter(e => e instanceof ValueChangeEvent))
Im Router ist es möglich, einen Errorhandler anzugeben, der Navigation-Errors abfängt, damit diese durch die Anwendung behandelt werden können. Der Navigation-Errorhandler muss beim Setup des Router über die Funktion “withNavigationErrorHandler()” angegeben werden. Bisher konnte der Navigation-Error-Handler nur eine “void”-Funktion sein. Mit Angular 18 ist es nun auch möglich, dass der Navigation-Error-Handler ein “RedirectCommand” zurückgibt, sodass eine Error-Navigation durchgeführt werden kann. In Listing 13 ist beispielhaft gezeigt, wie das RedirectCommand genutzt wird, um bei einem Navigationsfehler auf die Route “/error” zu navigieren.
Listing 13: Router-Errorhandler mit RedirectCommand
export const appConfig: ApplicationConfig = {providers: [
provideRouter(
appRoutes,
withRouterConfig({ resolveNavigationPromiseOnError: true }),
withNavigationErrorHandler(
() => new RedirectCommand(inject(Router).parseUrl('/error')),
),
),
])
Eine wichtige Ankündigung des Angular-Teams bezieht sich auf den zukünftigen Entwicklungspfad von Angular, speziell im Vergleich zu anderen, von Google ansonsten genutzten Web-Frameworks. So gibt es neben Angular z.B. noch das interne Google-”Wiz”-Framework. Während Angular von Google eher für geschäftliche und dynamische Anwendungen wie die Google Cloud Console oder Google Analytics verwendet wird, wird Wiz für Anwendungen wie die Google-Suche oder Youtube verwendet, für die vor allem die (initiale Load-)Perfomance wichtig ist. Da Google erkannt hat, dass sich die Anforderungen an neue Apps aber immer mehr überdecken, werden beide Framework-Teams in Zukunft deutlich enger zusammenarbeiten. Das äußert sich für Angular zum Beispiel darin, dass die Serversite-Rendering-Funktionalität (SSR) von Angular deutlich verbessert wurde und auch weiterhin wird. So wird nun z.B. die Bibliothek aus Wiz für Angular zur Verfügung gestellt. “jsaction” wird in SSR-Anwendungen genutzt, um User-Events zu cachen, die vor der vollständigen Hydrierung der Anwendung (also dem Start des dynamischen Teils) erfolgt sind. Nachdem die Anwendung dann im Client läuft, werden diese User-Events wieder abgespielt (“replay”).
Als weitere Verbesserung der SSR-Eigenschaften und damit auch der initialen Lade-Performance von Angular wurde die Client-Hydration als solches erweitert. Dem Client-Hydration-Feature aus Angular 17 wird mit Version 18 die Möglichkeit hinzugefügt, das Angular-I18N-Feature (Angular Internationalization) im Zusammenspiel mit der Hydration zu nutzen. Dazu muss die Funktion “withI18nSupport()” beim Einstellen der Client-Hydration mit angegeben werden, siehe dazu Listing 14.
Listing 14: Client-Hydration mit Angular I18N-Support
bootstrapApplication(AppComponent, {
providers: [provideClientHydration(withI18nSupport())]
});
Um API-Calls auch in komplexen Systemumgebungen zwischen Client- und Server-App cachen zu können, wurde das Injection-Token “HTTP_TRANSFER_CACHE_ORIGIN_MAP” exportiert. Damit kann ein Mapping zwischen Server- und Client-Origins angegeben werden. Dies ist nötig, wenn eine SSR-Anwendung für API-Calls auf dem Server andere Origins aufrufen muss als die Client-Anwendung zur Laufzeit. Wenn Calls eine Autorisierung benötigen, werden solche Calls jedoch standardmäßig nicht mehr gecachet. Falls dies doch gewünscht ist, gibt es die Option “includeRequestsWithAuthHeaders”, die beim Erzeugen des Cache (per “withHttpTransferCache()”) mitgegeben werden kann.
Die neuen APIs zum Erzeugen des HTTP-Client werden vom Angular-Team mittlerweile als stabil angesehen. Das betrifft allen voran die “provideHttpClient()”-Funktion und deren Plugins. Daher werden mit Angular 18 die älteren APIs als deprecated markiert. Im speziellen sind das das “HttpClientModule”, das “HttpClientXsrfModule” und das “HttpClientJsonpModule”. Diese können einfach durch die jeweilige provide-Funktion ersetzt werden. Falls bisher HTTP-Interceptors verwendet werden (provided mit dem “HTTP_INTERCEPTS”-InjectionToken), so sollte bei der Migration auf die neue Syntax das Plugin “withInterceptorsFromDi()” mit angegeben werden, siehe Listing 15.
Um die Transition möglichst einfach zu gestalten, wird außerdem eine automatische Migration durch das Angular-Team bereitgestellt. Die Migration sorgt bei einem “ng update” dafür, dass die alte Syntax in die neue überführt wird.
Listing 15: HttpClient mit DI-Interceptors konfigurieren
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient(withInterceptorsFromDi())]
};
Breaking Changes
Mit Angular 18 werden auch wieder einige alte, bisher als deprecated markierte APIs entfernt, um das Angular-Framework möglichst einfach und geradlinig zu halten. Dafür wird zunächst einmal der TypeScript-Support für alle TypeScript-Versionen kleiner als 5.4 eingestellt.
Weiterhin wurden einige Optimierungen in Bezug auf die Change-Detection durchgeführt. Das sollte in den meisten Anwendungen keine Auswirkungen haben, kann aber in einigen Grenzfällen zu Fehlern führen. Ein solcher Fall bezieht sich auf die Root-Komponente einer Anwendung (typischerweise “AppComponent”). Wenn bei dieser die ChangeDetection auf OnPush eingestellt ist und die Komponente HostBindings besitzt, wurden die HostBindings bisher auch dann überprüft, wenn die Komponente nicht als dirty (also “geändert”) gekennzeichnet war. Mit Version 18 werden solche Bindings nur noch gecheckt, wenn die Komponente selbst auch als dirty markiert ist.
Um feingranulare Signal-Change-Detection zu ermöglichen, wurde die ChangeDetection auch so verändert, dass nach einem ChangeDetection-Zyklus so lange weiter gecheckt wird, wie es Anwendungsteile gibt, die noch für ChangeDetection markiert sind. Das kann in bestimmten Situationen zu fehlerhaftem Verhalten führen, wenn Views mit “ChangeDetectorRef.detectChanges” immer wieder aufs Neue markiert werden. In solchen Fällen wird ein Fehler mit Fehlercode “NG0103” geworfen. Tatsächlich kann es aber auch dazu kommen, dass in Fällen, in denen vorher ein “ExpressionChangedAfterItHasBeenChecked”-Error gekommen wäre, in Zukunft die Change-Detection normal durchläuft. Das liegt daran, dass die Views ja so lange weiter gecheckt werden, bis es keine Dirty Views mehr gibt. Auch dies kann in sehr speziellen Fällen zu unerwartetem Verhalten führen. Das Angular-Team hat beispielsweise von einer Komponente berichtet, die durch diesen Mechanismus ungewollt zu früh initialisiert wurde. Vermutlich ist dies aber als eher seltener Fall zu bewerten.
Auch im Router gibt es mit Angular 18 einige Breaking Changes, die auf neuen Features beruhen. Als Breaking Change fallen diese Features aber nur dann auf, wenn die Router-APIs sehr intensiv genutzt werden. So können Guards nun nicht nur ein Boolean oder einen UrlTree zurückgeben, sondern mit Angular 18 auch ein “RedirectCommand”. Das Besondere am RedirectCommand ist, dass man damit auch spezifizieren kann, mit welchen “NavigationBehaviorOptions” (also z.B. “replaceUrl”) die Navigation stattfinden soll. Ein einfacher CanActivate-Guard, der ein Redirect-Command zurückgibt, ist in Listing 16 gezeigt.
Listing 16: Einfacher Guard mit Redirect-Command
export const loginGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
return new RedirectCommand(
router.parseUrl('/login'),
{replaceUrl: true}
);
};
Ein weiteres neues Feature betrifft Redirects, die direkt in der Routen-Konfiguration angegeben sind. Bisher konnte die “redirectTo”-Angabe innerhalb einer Redirect-Route nur ein fixer String sein. Nun sind hier auch Funktionen möglich, die Teile des ActivatedRouteSnapshot übergeben bekommen und entweder wiederum einen String oder einen UrlTree zurückgeben können. Ein einfaches Beispiel, das einen Query-Parameter aus den Snapshot-Daten ausliest, ist in Listing 17 gezeigt.
Listing 17: Redirect mit Redirect-Funktion
{
path: '',
pathMatch: 'full',
redirectTo: redirectData => {
const parentId = redirectData.queryParams['id'];
const router = inject(Router);
return router.parseUrl('/user/' + parentId);
}
}
Auch im Angular-Testing-Paket gibt es ein paar Breaking Changes, die sich zum einen auf die aktualisierten Change-Detection-Mechanismus von Angular allgemein beziehen. Zum anderen wurde die Test-Change-Detection in der Angular-Test-Umgebung näher an die “echte” App-Change-Detection angepasst. Das kann sich vor allem dann bemerkbar machen, wenn die Tests sehr sensitiv auf das Timing im Change-Detection-Zyklus reagieren. In einem solchen Fall müssen Tests ggf. auf die neue Situation angepasst werden.
In einem solchen Kontext wird zum Warten auf den Change-Detection-Zyklus gerne das “ComponentFixture.whenStable”-Promise genutzt. Dieses Promise wurde so angepasst, dass es nun dem “ApplicationRef.isStable” entspricht. Daher kann es mit Angular 18 passieren, dass das Promise nicht in allen Fällen resolved. Das passiert vor allem dann, wenn noch Router-Navigationen oder HttpClient-Requests offen sind. In solchen Fällen muss dann über die entsprechenden Mocks (z.B. “HttpTestingController.verify()”) sichergestellt werden, dass die Requests vor Aufruf des “whenStable” abgeschlossen sind. Alternativ muss eine andere geeignete Bedingung als “ComponentFixture.whenStable” zum Warten verwendet werden.
Schlussendlich wurde noch die “async()”-Funktion entfernt, die früher zum Erzeugen der asynchronen Angular-Testing-Umgebung verwendet wurde. Stattdessen sollte nun “waitForAsync()” verwendet werden.
Weitere APIs, die weggefallen sind, betreffen ehemals verwendete SSR-APIs. So wurde der deprecated “platformDynamicServer” entfernt. Genutzt werden sollte der “platformServer”. Auch das “ServerTransferStateModule” wurde in der Vergangenheit deprecated und wird nun entfernt, der “TransferState” kann auch ohne dieses Modul verwendet werden.
Stay tuned
Bei neuen Artikel & Eventupdates informiert werden:
Fazit
Angular 18 markiert einen weiteren Fortschritt in der Evolution des Frameworks, indem es eine Vielzahl neuer Funktionen und Optimierungen einführt. Diese Version macht wichtige Control-Flow-APIs und Deferrable-Views stabil, außerdem wird alles für die Nutzung von Angular ohne Zone.js vorbereitet. Die verbesserten SSR-Eigenschaften und die neue Client-Hydration sorgen für eine schnellere und effizientere Ausführung von Anwendungen. Trotz einiger Breaking Changes bieten die neuen Versionen viele Vorteile und Anpassungen, die die Entwicklung und Wartung von Angular-Anwendungen deutlich vereinfachen und verbessern. Es lohnt sich insofern, bei dem Update mitzumachen, da eine typische Anwendung in überschaubarem Maße angepasst werden muss und man damit verhindert, dass es zu einem Update-Rückstau kommt. Wer von den Features nicht direkt Gebrauch macht, kann jedoch auch mit dem Update auf die nächste Version warten.
Entwickler können sich auf eine robuste, zukunftssichere Plattform freuen, die kontinuierlich weiterentwickelt wird, um den wachsenden Anforderungen der modernen Webentwicklung gerecht zu werden.
Als Herausforderung zeichnet sich jedoch ab, dass durch die zunehmende Einführung neuer APIs Angular immer schwerer zu lernen wird, denn die wenigsten Projekte starten auf der grünen Wiese. Entwickler müssen somit alte und neue Konzepte parallel beherrschen. Damit geht einer der größten Vorteile von Angular verloren, nämlich dass es für eine Sache genau ein Konzept gibt und es nicht, wie beispielsweise oft in React-Projekten zu beobachten, unterschiedlichste Umsetzungsvarianten und APIs für dieselben zugrundeliegenden Anforderungen. Hier darf man sehr gespannt sein, wie das Angular-Team mit dem Spannungsfeld Rückwärtskompatibilität und “optionalen neuen Konzepten” im Gegensatz zu klaren, opinionated Strukturen und kognitivem Ballast umgehen wird.